﻿module MidiOperations

open System
open System.Collections.Generic
open System.Linq
open MidiDecoder
open NAudio.Wave
open SimpleFMMatrix.DataStructures

let PitchToFrequency (pitch : byte) =
    440.0 * (2.0 ** ((float pitch - 69.0) / 12.0))

let VelocityToAmplitude (velocity : byte) =
    let sqrtamp = (19.0 / 2520.0) * float velocity + 107.0 / 2520.0
    sqrtamp * sqrtamp * 0.1

type OutNote (velocity : byte, pitch : byte, matrix) =
    inherit FMNote (PitchToFrequency pitch, matrix, VelocityToAmplitude velocity)
    member val NoteOnSample = 0 with get, set
    member val NoteOffSample = 0 with get, set
    member this.Pitch = pitch
    member this.ReadMonoFloats (buffer : float32[], locks : obj [], bufferlength, startsampleindex) =
        let onindex = this.NoteOnSample - startsampleindex
        let offindex = this.NoteOffSample - startsampleindex
        let startindex = if onindex < 0 then 0 else onindex
        let releaseindex =
            if offindex < 0 then 0
            else if offindex >= bufferlength then bufferlength
            else offindex
        for h in startindex .. releaseindex - 1 do
            let result = float32 (this.Next ())
            lock locks.[h] (fun () -> buffer.[h] <- buffer.[h] + result)
        if not (offindex < 0 || offindex >= bufferlength) then
            this.Release ()
        for h in releaseindex .. bufferlength - 1 do
            let result = float32 (this.Next ())
            lock locks.[h] (fun () -> buffer.[h] <- buffer.[h] + result)
    override this.ToString () =
        this.NoteOnSample.ToString () + " - " + this.NoteOffSample.ToString ()

let Flatten (midi : MIDI, matrix) =
    if midi.Header.IsFPS then raise (NotSupportedException "FPS format is not supported.")
    let ticksperbeat = midi.Header.TimeDivision
    let unclosednotes = List<OutNote> ()
    let pedallednotes = List<OutNote> ()
    let result = List<OutNote> ()
    let mutable pedalon = false
    let mutable currBPM = 130.0
    let mutable currBPMStartTime = 0.0
    let mutable currBPMStartTick = 0
    let mutable endTick = 0
    let FromTimeOffset (newTick, currBPM, currBPMStartTime, currBPMStartTick) =
        float (newTick - currBPMStartTick) / float ticksperbeat / currBPM * 60.0 * InternalTypes.FloatSampleRate + currBPMStartTime
    for e in midi.Track |> Seq.concat |> Seq.sortBy (fun e -> e.TimeOffset) do
        match e with
        | :? TempoEvent as e ->
            currBPMStartTime <- FromTimeOffset (e.TimeOffset, currBPM, currBPMStartTime, currBPMStartTick)
            currBPMStartTick <- e.TimeOffset
            currBPM <- float MIDI.MicroSecondsPerMinute / float e.MicrosecondsPerQuarterNote
        | e when e :? NoteOffEvent || (e :? NoteOnEvent && (e :?> NoteOnEvent).Velocity = 0uy) ->
            let pitch = if e :? NoteOnEvent then (e :?> NoteOnEvent).Pitch else (e :?> NoteOffEvent).Pitch
            let ni = unclosednotes.FindIndex (fun n -> n.Pitch = pitch)
            if ni >= 0 then
                let n = unclosednotes.[ni]
                n.NoteOffSample <- int (FromTimeOffset (e.TimeOffset, currBPM, currBPMStartTime, currBPMStartTick))
                let _ = unclosednotes.Remove n
                ()
        | :? NoteOnEvent as e ->
            let n = OutNote (e.Velocity, e.Pitch, matrix)
            n.NoteOnSample <- int (FromTimeOffset (e.TimeOffset, currBPM, currBPMStartTime, currBPMStartTick))
            unclosednotes.Add n
            if pedalon then pedallednotes.Add n
            result.Add n
        | e when e :? ControlChangeEvent && (e :?> ControlChangeEvent).Control = 64uy ->
            let ee = e :?> ControlChangeEvent
            match (ee.Value, pedalon) with
            | (v, false) when v >= 64uy ->
                pedalon <- true
                pedallednotes.AddRange unclosednotes
            | (v, true) when v < 64uy ->
                pedalon <- false
                for n in pedallednotes do
                    n.NoteOffSample <- int (FromTimeOffset (e.TimeOffset, currBPM, currBPMStartTime, currBPMStartTick))
                pedallednotes.Clear ()
            | _ -> ()
        | :? EndOfTrack as e ->
            endTick <- Math.Max (e.TimeOffset, endTick)
        | _ -> ()
    let endSample = int (Math.Ceiling (FromTimeOffset (endTick, currBPM, currBPMStartTime, currBPMStartTick)))
    for n in unclosednotes do
        n.NoteOffSample <- endSample
    unclosednotes.Clear ()
    pedallednotes.Clear ()
    (result, endSample)

let ConvertAndWriteToWave (midi : MIDI, matrix : FMMatrix, outpath : string) =
    let (ns, endSample) = Flatten (midi, matrix)
    let mutable nsIndex = 0
    let writer = new WaveFileWriter (outpath, WaveFormat.CreateIeeeFloatWaveFormat(InternalTypes.SampleRate, 1))
    let bufferlength = 4096
    let buffer : float32[] = Array.zeroCreate bufferlength
    let locks : obj[] = Array.init bufferlength (fun i -> obj ())
    let mutable currSample = 0
    let onNotes = List<OutNote> ()
    while nsIndex < ns.Count || onNotes.Count > 0 do
        let currSampleCache = currSample
        let nextSample = currSample + bufferlength
        while nsIndex < ns.Count && ns.[nsIndex].NoteOnSample < nextSample do
            onNotes.Add ns.[nsIndex]
            nsIndex <- nsIndex + 1
        for i in 0 .. bufferlength - 1 do
            buffer.[i] <- 0.0f
        let _ =
            onNotes
            |> Seq.map (fun n ->
                async { n.ReadMonoFloats (buffer, locks, bufferlength, currSampleCache) })
            |> Async.Parallel
            |> Async.RunSynchronously
        let _ = onNotes.RemoveAll (fun n -> n.NoteEnded)
        writer.WriteSamples (buffer, 0, bufferlength)
        currSample <- nextSample
        if currSample % (bufferlength * 10) = 0 then
            Console.WriteLine (currSample.ToString () + " / " + endSample.ToString ())
    writer.Close ()
    Console.WriteLine "Done."
    
